Skip to content

feat: add threshold filtering to knn search - #2531

Open
wxyucs wants to merge 1 commit into
antgroup:mainfrom
wxyucs:codex/issue-2081-knn-threshold
Open

feat: add threshold filtering to knn search#2531
wxyucs wants to merge 1 commit into
antgroup:mainfrom
wxyucs:codex/issue-2081-knn-threshold

Conversation

@wxyucs

@wxyucs wxyucs commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add inclusive top-level distance threshold filtering for KNN in the maintained indexes only:

  • BruteForce
  • HGraph
  • IVF
  • Pyramid
  • SINDI
  • SIMQ

HNSW and DiskANN are deprecated and explicitly unsupported non-goals for this PR. Their source, test, and documentation paths are absent from the full PR diff.

Threshold filtering excludes non-finite values before bounded result selection and allows eligible finite candidates to backfill. Non-reorder IVF, HGraph, Pyramid, and SINDI apply eligibility before bounded selection; reordered paths apply the bound to precise distances. HGraph iterator search consumes rejected pages and retained discard state internally. Ordinary KNN and range search preserve eligible infinite results while non-finite graph nodes remain safe traversal seeds. Threshold scratch storage uses the active request allocator.

Validation

  • One Conventional Commit with DCO and Assisted-by trailers.
  • clang-format-15, direct clang-tidy-15, bounded debug build, and git diff --check passed.
  • Bounded threshold regressions passed: 159 assertions in 11 cases.
  • Basic and parallel searcher suites passed: 243,441 assertions in 6 cases.
  • The exact CircleCI 15792 dirty-vector seed and the daily variant passed: 6,226 assertions in 2 cases.
  • The full PR path diff has zero HNSW/DiskANN source, test, or documentation paths.
  • All 52 review threads were answered inline and resolved.

Fixes: #2081

Copilot AI review requested due to automatic review settings July 24, 2026 07:51
@wxyucs wxyucs added kind/feature Brand-new functionality or capabilities 引入全新的功能、新特性或新能力 version/1.1 labels Jul 24, 2026
@vsag-bot

vsag-bot commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

/label status/ready-to-merge
/waiting-on maintainer
/request-review @jiaweizone
/request-review @inabao

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vsag-bot
vsag-bot self-requested a review July 24, 2026 07:51
@vsag-bot

vsag-bot commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Automated pull request review completed.

Review effort: high (1623 changed lines across 34 files).

Submitted 2 inline comments.
Review: #2531 (review)

@mergify

mergify Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 All 3 merge protections satisfied — ready to merge.

Show 3 satisfied protections

🟢 Require kind label

  • label~=^kind/

🟢 Require version label

  • label~=^version/

🟢 Require linked issue for feature/bug PRs

  • body~=(?im)(?:^|[\s\-\*])(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s+(?:#\d+|[\w.\-]+/[\w.\-]+#\d+|https?://github\.com/[\w.\-]+/[\w.\-]+/issues/\d+)

LHT129

This comment was marked as resolved.

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: medium (246 changed lines across 13 files).
Submitted 3 inline comments.

Reviewed commit 9b6cf96.

Comment thread src/algorithm/ivf/ivf.cpp Outdated
Comment thread include/vsag/search_request.h Outdated
Comment thread src/utils/search_threshold.h Outdated
Copilot AI review requested due to automatic review settings July 24, 2026 11:12
@wxyucs
wxyucs force-pushed the codex/issue-2081-knn-threshold branch from 9b6cf96 to e718e38 Compare July 24, 2026 11:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vsag-bot
vsag-bot self-requested a review July 24, 2026 11:12

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: high (397 changed lines across 15 files).
Submitted 7 inline comments.

Reviewed commit e718e38.

Comment thread src/utils/search_threshold.h
Comment thread src/utils/search_threshold.h Outdated
Comment thread src/algorithm/inner_index_interface.cpp Outdated
Comment thread src/algorithm/hgraph/hgraph_search.cpp Outdated
Comment thread src/algorithm/ivf/ivf.cpp
Comment thread src/algorithm/ivf/ivf.cpp
Comment thread docs/docs/en/src/guide/knn_search.md
@wxyucs
wxyucs force-pushed the codex/issue-2081-knn-threshold branch from e718e38 to d05a524 Compare July 27, 2026 04:04
Copilot AI review requested due to automatic review settings July 27, 2026 04:04
@vsag-bot
vsag-bot self-requested a review July 27, 2026 04:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment thread src/utils/search_threshold.h

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This update addresses all six issues raised in the previous review round:

  1. IVF reorder threshold — Threshold is now applied after exact-distance reorder, with FilterDatasetByThreshold capping at topk_. The candidate pool is retained at full size when threshold is present.
  2. Non-finite threshold validationValidateSearchThreshold rejects NaN/Inf in SearchWithRequest paths, and index_impl.h validates inside SAFE_CALL for consistent error handling.
  3. Allocator preservationFilterDatasetByThreshold accepts an Allocator*, allocates through it, and sets Owner(true, allocator). HNSW and SINDI pass their caller allocator; a regression test verifies balanced deallocation.
  4. route_buckets_only threshold — IVF now explicitly rejects threshold with disable_bucket_scan via CHECK_ARGUMENT.
  5. IVF reasoning reportAttachReasoningReport is called after FilterDatasetByThreshold in the reorder path.
  6. Pyramid/SIMQ threshold — Both now parse and apply threshold via FilterDatasetByThreshold with their allocators and k-capping.

Additional observations:

  • The #include <unordered_set> addition in test_brute_force.cpp fixes a missing-include bug in the existing code (it was used but not included).
  • ParseSearchThreshold now validates JSON type via IsNumber() before calling GetFloat(), preventing silent coercion of string values.
  • The SetOFFFirstUsed() call in HGraph when threshold filtering empties the result is a correct fix to maintain iterator filter state consistency.
  • FilterDatasetByThreshold correctly preserves statistics and reasoning from the input dataset.

One minor suggestion was posted inline about the two-pass iteration in FilterDatasetByThreshold.

Comment thread docs/docs/zh/src/guide/knn_search.md

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: high (1177 changed lines across 27 files).
Submitted 4 inline comments.

Reviewed commit b35bbc3.

Comment thread src/algorithm/hgraph/hgraph_search.cpp
Comment thread src/impl/searcher/basic_searcher.cpp
Comment thread src/impl/searcher/basic_searcher.cpp Outdated
Comment thread src/algorithm/simq/simq.cpp
Copilot AI review requested due to automatic review settings August 3, 2026 04:06
@wxyucs
wxyucs force-pushed the codex/issue-2081-knn-threshold branch from b35bbc3 to eb2d560 Compare August 3, 2026 04:06
@vsag-bot
vsag-bot self-requested a review August 3, 2026 04:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

The current top-level threshold validation in IndexImpl appears to change legacy (unsupported) index behavior and the new threshold utility contains dead/unreachable code that should be corrected.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details

Suppressed comments (3)

src/utils/search_threshold.h:120

  • The else if (extra_infos == nullptr && extra_size > 0 && input_extra_infos != nullptr) branch is effectively unreachable: when result_count > 0, extra_size > 0, and input_extra_infos is non-null, AllocateThresholdArray<char>(result_count * extra_size, ...) either returns a non-null pointer or throws, so extra_infos cannot remain null. This dead code makes the extra-info handling harder to reason about.
    if (result_count == 0) {
        result->ExtraInfos(nullptr)->ExtraInfoSize(0);
    } else if (extra_infos == nullptr and extra_size > 0 and input_extra_infos != nullptr) {
        result->ExtraInfos(nullptr)->ExtraInfoSize(extra_size);
    }

src/index/index_impl.h:591

  • ValidateThresholdParameters() is invoked for all KnnSearch overloads and always calls ParseSearchThreshold(parameters). That means legacy/unsupported indexes (e.g. HNSW/DiskANN) will now start rejecting requests like { "threshold": "bad" } even though they don’t implement threshold filtering (and their own parameter parsing would otherwise ignore the field). This contradicts the docs/PR statement that threshold support does not add or change behavior for those legacy indexes. Consider only validating threshold on the maintained indexes that actually support it.
    ValidateThresholdParameters(const std::string& parameters) const {
        try {
            ParseSearchThreshold(parameters);
            return {};
        } catch (const VsagException& e) {

docs/docs/en/src/guide/knn_search.md:95

  • Grammar: “are explicit unsupported non-goals” should be “are explicitly unsupported non-goals”.
Threshold filtering is supported by the maintained indexes: BruteForce, HGraph, IVF, Pyramid,
SINDI, and SIMQ. HNSW and DiskANN are deprecated and are explicit unsupported
non-goals; this option does not add or change behavior for either legacy index.
  • Files reviewed: 27/27 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: high (1276 changed lines across 27 files).
Submitted 6 inline comments.

Reviewed commit eb2d560.

Comment thread src/algorithm/hgraph/hgraph_search.cpp
Comment thread src/algorithm/hgraph/hgraph_search.cpp
Comment thread src/impl/searcher/basic_searcher.cpp Outdated
Comment thread src/algorithm/pyramid/pyramid.cpp
Comment thread src/algorithm/sindi/sindi.cpp
Comment thread src/algorithm/simq/simq.cpp
Comment thread src/impl/searcher/basic_searcher.cpp Outdated
Comment thread src/utils/search_threshold.h

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this comprehensive PR. The threshold filtering feature is well-implemented across all six maintained indexes with thorough test coverage (109 assertions in 9 threshold-specific cases, plus full unit suite passing).

I reviewed the full diff and found only minor suggestions beyond what has already been addressed in the 30 existing review threads:

  1. [suggestion] The traversal_priority and is_result_distance_eligible helpers are duplicated in basic_searcher.cpp and parallel_searcher.cpp. Consider extracting to a shared header.

  2. [note] An unreachable defensive branch in FilterDatasetByThreshold (the else if for extra_infos == nullptr).

The core logic — non-finite distance filtering, threshold validation at the IndexImpl layer, IVF reorder/non-reorder paths, HGraph iterator page consumption, and the graph searcher traversal changes — all look correct. The GetNumElements()GetDim() fix in AttachReasoningReport is a good catch.

No blocking issues found.

Copilot AI review requested due to automatic review settings August 3, 2026 08:04
@wxyucs
wxyucs force-pushed the codex/issue-2081-knn-threshold branch from eb2d560 to 04228c0 Compare August 3, 2026 08:04
@vsag-bot
vsag-bot self-requested a review August 3, 2026 08:04
Signed-off-by: Xiangyu Wang <wxy407827@antgroup.com>
Assisted-by: OpenAI Codex:GPT-5

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated no new comments.

Suppressed comments (7)

tests/test_simq.cpp:1

  • This file uses std::numeric_limits and std::isnan, but does not include <limits> / <cmath> directly in the shown includes. Relying on transitive includes (via other project headers) can make the build fragile across platforms/standard libraries; add the needed standard headers explicitly.
    docs/docs/en/src/guide/knn_search.md:95
  • Grammar: 'explicit unsupported' should be 'explicitly unsupported' to read correctly.
Threshold filtering is supported by the maintained indexes: BruteForce, HGraph, IVF, Pyramid,
SINDI, and SIMQ. HNSW and DiskANN are deprecated and are explicit unsupported
non-goals; this option does not add or change behavior for either legacy index.

src/index/index_impl.h:309

  • IndexImpl now parses JSON in ValidateThresholdParameters() purely to validate threshold. For indexes that later call ParseSearchThreshold(parameters) again (e.g., to populate SearchRequest::threshold_), this introduces redundant JSON parsing on every KNN call. Consider restructuring so the threshold is parsed once and reused (e.g., parse once in IndexImpl and store in SearchParam/SearchRequest, or validate using the already-parsed threshold_ path for the request-based APIs).
              BitsetPtr invalid = nullptr) const override {
        auto threshold_validation = ValidateThresholdParameters(parameters);
        if (not threshold_validation.has_value()) {
            return tl::unexpected(threshold_validation.error());
        }

src/algorithm/simq/simq.cpp:793

  • Because reranked is sorted by ascending distance (and NaNs are ordered last), when threshold is present you can break early once you encounter the first finite distance > threshold—no later finite entries can pass the threshold. This avoids scanning potentially large reranked vectors in cases where the threshold excludes most results.
    int64_t result_count = 0;
    for (const auto& [distance, _] : reranked) {
        if (result_count >= k) {
            break;
        }
        if (not threshold.has_value() or
            (std::isfinite(distance) and distance <= threshold.value())) {
            ++result_count;
        }
    }

tests/test_brute_force.cpp:57

  • allocation_count_/AllocationCount() reads like it might represent currently active allocations, but it actually tracks a monotonically increasing total number of successful allocations. Renaming to something like total_allocation_count_ (or adding a short comment) would prevent misinterpretation in future tests.
        if (ptr != nullptr) {
            allocations_[ptr] = size;
            allocated_bytes_ += size;
            allocation_count_ += 1;
        }

tests/test_brute_force.cpp:113

  • allocation_count_/AllocationCount() reads like it might represent currently active allocations, but it actually tracks a monotonically increasing total number of successful allocations. Renaming to something like total_allocation_count_ (or adding a short comment) would prevent misinterpretation in future tests.
    uint64_t
    AllocationCount() const {
        std::scoped_lock lock(mutex_);
        return allocation_count_;
    }

tests/test_brute_force.cpp:125

  • allocation_count_/AllocationCount() reads like it might represent currently active allocations, but it actually tracks a monotonically increasing total number of successful allocations. Renaming to something like total_allocation_count_ (or adding a short comment) would prevent misinterpretation in future tests.
    uint64_t allocation_count_{0};

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

All 14 previous review comments have been addressed and resolved in commit 04228c00. The current code state looks correct across all reviewed dimensions:

Correctness

  • Threshold filtering is correctly applied after reorder in IVF (exact distances), before reorder in the bucket search path (approximate distances with enable_reorder=false), and via FilterDatasetByThreshold in SINDI/Pyramid
  • Non-finite distance handling in basic_searcher and parallel_searcher correctly uses traversal_priority() for heap ordering and is_result_distance_eligible() for result inclusion
  • FilterDatasetByThreshold properly handles the result_count == 0 case (null ExtraInfos, zero ExtraInfoSize)
  • AllocateThresholdArray correctly returns nullptr for zero-count and throws std::bad_alloc on allocator failure
  • ValidateThresholdParameters is called before empty-index checks in all 5 KnnSearch overloads and SearchWithRequest
  • IVF route_buckets_only path explicitly rejects threshold with CHECK_ARGUMENT
  • HGraph iterator path properly consumes threshold-rejected candidates via SetPoint and retries internally on empty pages
  • hgraph_build.cpp defensively checks Empty() before Top() on route graph search results

Test Coverage

  • BruteForce: threshold filtering, non-finite distances, NaN base vectors, IP metric, allocator ownership, empty index validation, malformed JSON rejection
  • HGraph: iterator threshold consumption, non-finite entry distances, non-finite candidate filtering, infinity preservation without threshold, build tolerance for non-finite route probes
  • Searcher unit tests: non-finite distance bridge traversal for both basic and parallel searchers
  • FilterDatasetByThreshold unit tests: allocator ownership, extra-info preservation, non-finite input, allocation failure

No New Issues Found

The implementation is consistent across all 6 maintained indexes. No blocking issues, no suggestions worth flagging.

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary (commit 04228c0)

All 14 previous review comments have been addressed and resolved. After thorough review of all 30 changed files, no new issues were found. The implementation is consistent across all 6 maintained indexes (BruteForce, HGraph, IVF, Pyramid, SINDI, SIMQ).

Key areas verified:

  • Threshold filtering is correctly applied after reorder in IVF, and before reorder in bucket search path
  • Non-finite distance handling in basic_searcher/parallel_searcher correctly uses traversal_priority() and is_result_distance_eligible()
  • FilterDatasetByThreshold properly handles result_count==0, allocator ownership, and extra-info preservation
  • ValidateThresholdParameters is called before empty-index checks in all KnnSearch overloads
  • IVF route_buckets_only explicitly rejects threshold
  • HGraph iterator path consumes threshold-rejected candidates via SetPoint
  • hgraph_build.cpp defensively checks Empty() before Top()

Test coverage includes BruteForce, HGraph, searcher unit tests, and FilterDatasetByThreshold unit tests covering threshold filtering, non-finite distances, NaN, IP metric, allocator ownership, and allocation failure.

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary (commit 04228c0)

All 14 previous review comments have been addressed and resolved. After thorough review of all 30 changed files, no new issues were found.

Key areas verified:

  • Threshold filtering correctly applied after reorder in IVF, before reorder in bucket search path
  • Non-finite distance handling in basic_searcher/parallel_searcher uses traversal_priority() and is_result_distance_eligible()
  • FilterDatasetByThreshold handles result_count==0, allocator ownership, extra-info preservation
  • ValidateThresholdParameters called before empty-index checks in all KnnSearch overloads
  • IVF route_buckets_only explicitly rejects threshold
  • HGraph iterator path consumes threshold-rejected candidates via SetPoint
  • hgraph_build.cpp defensively checks Empty() before Top()

Test coverage: BruteForce, HGraph, searcher unit tests, FilterDatasetByThreshold unit tests.

Copilot AI review requested due to automatic review settings August 3, 2026 08:27
@wxyucs
wxyucs force-pushed the codex/issue-2081-knn-threshold branch from 04228c0 to c9bceda Compare August 3, 2026 08:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated no new comments.

Suppressed comments (4)

docs/docs/en/src/guide/knn_search.md:95

  • Grammar issue: “are explicit unsupported” should be “are explicitly unsupported”. This reads as a typo in the public docs.
SINDI, and SIMQ. HNSW and DiskANN are deprecated and are explicit unsupported
non-goals; this option does not add or change behavior for either legacy index.

src/index/index_impl.h:310

  • Threshold parameter validation now runs before CHECK_QUERY_RETURN_EMPTY_DATASET(query). This changes observable behavior: previously an empty/invalid query could short-circuit to an empty dataset without parsing parameters, but now malformed threshold (or invalid JSON) can fail the call even when the query is empty. Consider moving the threshold validation after the empty-query fast path (and ideally after the empty-index fast path as well) so empty queries preserve existing behavior and avoid unnecessary JSON parsing.
        auto threshold_validation = ValidateThresholdParameters(parameters);
        if (not threshold_validation.has_value()) {
            return tl::unexpected(threshold_validation.error());
        }
        CHECK_QUERY_RETURN_EMPTY_DATASET(query);

src/index/index_impl.h:503

  • SAFE_CALL(...) now wraps multiple statements (including a conditional early return). This makes the control flow harder to read and increases the chance of subtle macro-related issues. A clearer structure would be to wrap only the throwing calls (e.g., ValidateSearchThreshold(...) and/or inner_index_->SearchWithRequest(...)) in separate SAFE_CALL invocations, keeping ordinary branching outside the macro.
        SAFE_CALL(ValidateSearchThreshold(request.threshold_);
                  if (GetNumElements() == 0 && !this->ShouldSkipEmptyCheck(request.params_str_)) {
                      return DatasetImpl::MakeEmptyDataset();
                  } return this->inner_index_->SearchWithRequest(request));

src/algorithm/simq/simq.cpp:793

  • When threshold is not set, this counts (and later may return) results with distance = NaN, because the eligibility check becomes unconditional. Other KNN paths in this PR explicitly treat NaN as ineligible/unordered. To keep SIMQ consistent and prevent returning NaN distances to callers, exclude NaNs regardless of whether threshold is present (e.g., require not std::isnan(distance) in both branches).
    int64_t result_count = 0;
    for (const auto& [distance, _] : reranked) {
        if (result_count >= k) {
            break;
        }
        if (not threshold.has_value() or
            (std::isfinite(distance) and distance <= threshold.value())) {
            ++result_count;
        }
    }

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: high (1623 changed lines across 34 files).
Submitted 2 inline comments.

Reviewed commit c9bceda.

bool enable_reorder) const {
if (threshold.has_value() and
(not std::isfinite(dist) or (not enable_reorder and 1.0F + dist > threshold.value()))) {
return false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Clear rejected SINDI scores before reusing the window buffer

search_impl reuses one dists array across mutable windows and relies on heap insertion to zero each processed slot, but this new early return leaves the rejected partial distance intact. With window_size=10000, a rejected first-window score of -0.1 is added to the next window's true -0.4 score, causing a threshold-0.5 query to return ID 10000 at 0.5 even though its true distance is 0.6. Clear dist before every threshold-rejection return; insert_candidate_into_heap has the same stale-slot path.

for (const auto& record : finite_records) {
search_result->Push(record);
}
filter_search_result_by_threshold(search_result, request.threshold_, ctx.alloc);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Reject non-finite exact distances before the reorder k cap

With precise reorder enabled, reorder(..., k) has already bounded the exact heap before this post-filter runs. In an SQ8-base/FP32-precise L2 index containing {NaN, 1, 0}, querying 1 with k=1, ef_search=3, and threshold 0 returned empty in 5 of 30 builds even though the distance-zero candidate was in the approximate pool: when the NaN exact distance enters FlattenReorder first, later finite comparisons against its heap top fail. Exclude non-finite exact distances inside bounded reorder selection, including iterator reorder, so eligible finite candidates can backfill.

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note] SINDI threshold comparison uses 1.0F + dist which accounts for the internal dist = -ip representation. This is correct for IP metric but would be incorrect for L2. If SINDI only supports IP, consider documenting this assumption or adding a metric guard to prevent future misuse when L2 support is added. (src/algorithm/sindi/sindi.cpp:736, src/datacell/sparse_term_datacell.cpp:75,124)

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note] The is_result_distance_eligible<KNN_SEARCH> function in searcher_utils.h rejects non-finite distances when a threshold is set, but only checks std::isfinite(distance). This means -inf distances (which are "better" than any finite distance) are also rejected. While -inf is unusual in practice, if it were to occur (e.g., from a degenerate vector), it would be incorrectly excluded from results. Consider using std::isnan instead of not std::isfinite for the threshold-aware check, or document that -inf is intentionally excluded. (src/impl/searcher/searcher_utils.h:30-31)

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note] In flat_bucket_searcher.cpp, the KNN path checks param.distance_threshold but the RANGE_SEARCH path does not. This is intentional since threshold is a KNN-only feature, but the asymmetry could be confusing to future readers. Consider adding a brief comment in the RANGE_SEARCH path noting that threshold filtering is intentionally not applied here. (src/algorithm/ivf/flat_bucket_searcher.cpp:71-72,99+)

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall Review

This PR adds optional inclusive distance threshold filtering to KNN search across all 6 maintained indexes (BruteForce, HGraph, IVF, Pyramid, SINDI, SIMQ). The implementation is thorough and well-tested after multiple rounds of review.

What works well

  1. Consistent API: std::optional<float> threshold flows through SearchRequestInnerSearchParam → individual index implementations. All indexes use the same ParseSearchThreshold/ValidateSearchThreshold utilities.

  2. NaN/inf safety: All threshold comparison paths use std::isfinite checks before comparing distances. The filter_search_result_by_threshold helper and is_result_distance_eligible template both guard against unordered float comparisons.

  3. Exception safety: FilterDatasetByThreshold attaches each allocation to the owning Dataset immediately, preventing leaks on allocation failure. The AllocateThresholdArray helper provides a unified allocation path.

  4. Test coverage: Every maintained index has threshold-specific tests covering: basic filtering, NaN handling, non-finite rejection, empty results, IP metric, iterator paths (HGraph), reorder paths (IVF), and allocator ownership.

  5. Review iteration quality: All issues from previous review rounds (iterator consumption, IVF reasoning report ordering, SIMQ statistics accuracy, empty-index JSON validation, NaN-safe heap filtering, allocator ownership) have been properly addressed.

Minor observations (non-blocking)

  1. SINDI 1.0F + dist pattern (sindi.cpp:736, sparse_term_datacell.cpp:75,124): Correctly accounts for the internal dist = -ip representation, but applies unconditionally regardless of metric type. If SINDI ever supports L2, this would silently produce wrong results.

  2. -inf rejection (searcher_utils.h:30-31): is_result_distance_eligible<KNN_SEARCH> rejects all non-finite distances when threshold is set, including -inf which is technically "better" than any finite distance. This is unlikely to occur in practice but worth noting.

  3. flat_bucket_searccher.cpp asymmetry: The KNN path has threshold filtering but the RANGE_SEARCH path does not — intentional but could benefit from a brief comment.

Verdict

The implementation is correct, well-tested, and all previously identified issues have been resolved. Approved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature Brand-new functionality or capabilities 引入全新的功能、新特性或新能力 module/api module/docs module/index module/testing size/XXL version/1.1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat](other): support threshold filtering in knn search

4 participants